slimecing

a fighting game featuring slimes and swords
Log | Files | Refs | README

VisualizationHelpers.cs (26364B)


      1 using System;
      2 using UnityEngine.InputSystem.Utilities;
      3 using Unity.Collections.LowLevel.Unsafe;
      4 using UnityEngine.InputSystem.LowLevel;
      5 
      6 ////REVIEW: for vector2 visualizers of sticks, it could be useful to also visualize deadzones and raw values
      7 
      8 namespace UnityEngine.InputSystem.Samples
      9 {
     10     internal static class VisualizationHelpers
     11     {
     12         public enum Axis { X, Y, Z }
     13 
     14         public abstract class Visualizer
     15         {
     16             public abstract void OnDraw(Rect rect);
     17             public abstract void AddSample(object value, double time);
     18         }
     19 
     20         public abstract class ValueVisualizer<TValue> : Visualizer
     21             where TValue : struct
     22         {
     23             public RingBuffer<TValue> samples;
     24             public RingBuffer<GUIContent> samplesText;
     25 
     26             protected ValueVisualizer(int numSamples = 10)
     27             {
     28                 samples = new RingBuffer<TValue>(numSamples);
     29                 samplesText = new RingBuffer<GUIContent>(numSamples);
     30             }
     31 
     32             public override void AddSample(object value, double time)
     33             {
     34                 var v = default(TValue);
     35 
     36                 if (value != null)
     37                 {
     38                     if (!(value is TValue val))
     39                         throw new ArgumentException(
     40                             $"Expecting value of type '{typeof(TValue).Name}' but value of type '{value?.GetType().Name}' instead",
     41                             nameof(value));
     42                     v = val;
     43                 }
     44 
     45                 samples.Append(v);
     46                 samplesText.Append(new GUIContent(v.ToString()));
     47             }
     48         }
     49 
     50         // Visualizes integer and real type primitives.
     51         public class ScalarVisualizer<TValue> : ValueVisualizer<TValue>
     52             where TValue : struct
     53         {
     54             public TValue limitMin;
     55             public TValue limitMax;
     56             public TValue min;
     57             public TValue max;
     58 
     59             public ScalarVisualizer(int numSamples = 10)
     60                 : base(numSamples)
     61             {
     62             }
     63 
     64             public override void OnDraw(Rect rect)
     65             {
     66                 // For now, only draw the current value.
     67                 DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
     68                 if (samples.count == 0)
     69                     return;
     70                 var sample = samples[samples.count - 1];
     71                 if (Compare(sample, default) == 0)
     72                     return;
     73                 if (Compare(limitMin, default) != 0)
     74                 {
     75                     // Two-way visualization with positive and negative side.
     76                     throw new NotImplementedException();
     77                 }
     78                 else
     79                 {
     80                     // One-way visualization with only positive side.
     81                     var ratio = Divide(sample, limitMax);
     82                     var fillRect = rect;
     83                     fillRect.width = rect.width * ratio;
     84                     DrawRectangle(fillRect, new Color(0, 1, 0, 0.75f));
     85 
     86                     var valuePos = new Vector2(fillRect.xMax, fillRect.y + fillRect.height / 2);
     87                     DrawText(samplesText[samples.count - 1], valuePos, ValueTextStyle);
     88                 }
     89             }
     90 
     91             public override void AddSample(object value, double time)
     92             {
     93                 base.AddSample(value, time);
     94 
     95                 if (value != null)
     96                 {
     97                     var val = (TValue)value;
     98                     if (Compare(min, val) > 0)
     99                         min = val;
    100                     if (Compare(max, val) < 0)
    101                         max = val;
    102                 }
    103             }
    104 
    105             private static unsafe int Compare(TValue left, TValue right)
    106             {
    107                 var leftPtr = UnsafeUtility.AddressOf(ref left);
    108                 var rightPtr = UnsafeUtility.AddressOf(ref right);
    109                 if (typeof(TValue) == typeof(int))
    110                     return ((int*)leftPtr)->CompareTo(*(int*)rightPtr);
    111                 if (typeof(TValue) == typeof(float))
    112                     return ((float*)leftPtr)->CompareTo(*(float*)rightPtr);
    113                 throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
    114             }
    115 
    116             private static unsafe void Subtract(ref TValue left, TValue right)
    117             {
    118                 var leftPtr = UnsafeUtility.AddressOf(ref left);
    119                 var rightPtr = UnsafeUtility.AddressOf(ref right);
    120 
    121                 if (typeof(TValue) == typeof(int))
    122                     *(int*)leftPtr = *(int*)leftPtr - *(int*)rightPtr;
    123                 if (typeof(TValue) == typeof(float))
    124                     *(float*)leftPtr = *(float*)leftPtr - *(float*)rightPtr;
    125                 throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
    126             }
    127 
    128             private static unsafe float Divide(TValue left, TValue right)
    129             {
    130                 var leftPtr = UnsafeUtility.AddressOf(ref left);
    131                 var rightPtr = UnsafeUtility.AddressOf(ref right);
    132 
    133                 if (typeof(TValue) == typeof(int))
    134                     return (float)*(int*)leftPtr / *(int*)rightPtr;
    135                 if (typeof(TValue) == typeof(float))
    136                     return *(float*)leftPtr / *(float*)rightPtr;
    137                 throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
    138             }
    139         }
    140 
    141         ////TODO: allow asymmetric center (i.e. center not being a midpoint of rectangle)
    142         ////TODO: enforce proper proportion between X and Y; it's confusing that X and Y can have different units yet have the same length
    143         public class Vector2Visualizer : ValueVisualizer<Vector2>
    144         {
    145             // Our value space extends radially from the center, i.e. we have
    146             // 360 discrete directions. Sampling at that granularity doesn't work
    147             // super well in visualizations so we quantize to 3 degree increments.
    148             public Vector2[] maximums = new Vector2[360 / 3];
    149             public Vector2 limits = new Vector2(1, 1);
    150 
    151             private GUIContent limitsXText;
    152             private GUIContent limitsYText;
    153 
    154             public Vector2Visualizer(int numSamples = 10)
    155                 : base(numSamples)
    156             {
    157             }
    158 
    159             public override void AddSample(object value, double time)
    160             {
    161                 base.AddSample(value, time);
    162 
    163                 if (value != null)
    164                 {
    165                     // Keep track of radial maximums.
    166                     var vector = (Vector2)value;
    167                     var angle = Vector2.SignedAngle(Vector2.right, vector);
    168                     if (angle < 0)
    169                         angle = 360 + angle;
    170                     var angleInt = Mathf.FloorToInt(angle) / 3;
    171                     if (vector.sqrMagnitude > maximums[angleInt].sqrMagnitude)
    172                         maximums[angleInt] = vector;
    173 
    174                     // Extend limits if value is out of range.
    175                     var limitX = Mathf.Max(Mathf.Abs(vector.x), limits.x);
    176                     var limitY = Mathf.Max(Mathf.Abs(vector.y), limits.y);
    177                     if (!Mathf.Approximately(limitX, limits.x))
    178                     {
    179                         limits.x = limitX;
    180                         limitsXText = null;
    181                     }
    182                     if (!Mathf.Approximately(limitY, limits.y))
    183                     {
    184                         limits.y = limitY;
    185                         limitsYText = null;
    186                     }
    187                 }
    188             }
    189 
    190             public override void OnDraw(Rect rect)
    191             {
    192                 DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
    193                 DrawAxis(Axis.X, rect, new Color(0, 1, 0, 0.75f));
    194                 DrawAxis(Axis.Y, rect, new Color(0, 1, 0, 0.75f));
    195 
    196                 var sampleCount = samples.count;
    197                 if (sampleCount == 0)
    198                     return;
    199 
    200                 // If limits aren't (1,1), show the actual values.
    201                 if (limits != new Vector2(1, 1))
    202                 {
    203                     if (limitsXText == null)
    204                         limitsXText = new GUIContent(limits.x.ToString());
    205                     if (limitsYText == null)
    206                         limitsYText = new GUIContent(limits.y.ToString());
    207 
    208                     var limitsXSize = ValueTextStyle.CalcSize(limitsXText);
    209                     var limitsXPos = new Vector2(rect.x - limitsXSize.x, rect.y - 5);
    210                     var limitsYPos = new Vector2(rect.xMax, rect.yMax);
    211 
    212                     DrawText(limitsXText, limitsXPos, ValueTextStyle);
    213                     DrawText(limitsYText, limitsYPos, ValueTextStyle);
    214                 }
    215 
    216                 // Draw maximums.
    217                 var numMaximums = 0;
    218                 var firstMaximumPos = default(Vector2);
    219                 var lastMaximumPos = default(Vector2);
    220                 for (var i = 0; i < 360 / 3; ++i)
    221                 {
    222                     var value = maximums[i];
    223                     if (value == default)
    224                         continue;
    225                     var valuePos = PixelPosForValue(value, rect);
    226                     if (numMaximums > 0)
    227                         DrawLine(lastMaximumPos, valuePos, new Color(1, 1, 1, 0.25f));
    228                     else
    229                         firstMaximumPos = valuePos;
    230                     lastMaximumPos = valuePos;
    231                     ++numMaximums;
    232                 }
    233                 if (numMaximums > 1)
    234                     DrawLine(lastMaximumPos, firstMaximumPos, new Color(1, 1, 1, 0.25f));
    235 
    236                 // Draw samples.
    237                 var alphaStep = 1f / sampleCount;
    238                 var alpha = 1f;
    239                 for (var i = sampleCount - 1; i >= 0; --i) // Go newest to oldest.
    240                 {
    241                     var value = samples[i];
    242                     var valueRect = RectForValue(value, rect);
    243                     DrawRectangle(valueRect, new Color(1, 0, 0, alpha));
    244                     alpha -= alphaStep;
    245                 }
    246 
    247                 // Print value of most recent sample. Draw last so
    248                 // we draw over the other stuff.
    249                 var lastSample = samples[sampleCount - 1];
    250                 var lastSamplePos = PixelPosForValue(lastSample, rect);
    251                 lastSamplePos.x += 3;
    252                 lastSamplePos.y += 3;
    253                 DrawText(samplesText[sampleCount - 1], lastSamplePos, ValueTextStyle);
    254             }
    255 
    256             private Rect RectForValue(Vector2 value, Rect rect)
    257             {
    258                 var pos = PixelPosForValue(value, rect);
    259                 return new Rect(pos.x - 1, pos.y - 1, 2, 2);
    260             }
    261 
    262             private Vector2 PixelPosForValue(Vector2 value, Rect rect)
    263             {
    264                 var center = rect.center;
    265                 var x = Mathf.Abs(value.x) / limits.x * Mathf.Sign(value.x);
    266                 var y = Mathf.Abs(value.y) / limits.y * Mathf.Sign(value.y) * -1; // GUI Y is upside down.
    267                 var xInPixels = x * rect.width / 2;
    268                 var yInPixels = y * rect.height / 2;
    269                 return new Vector2(center.x + xInPixels,
    270                     center.y + yInPixels);
    271             }
    272         }
    273 
    274         // Y axis is time, X axis can be multiple visualizations.
    275         public class TimelineVisualizer : Visualizer
    276         {
    277             public bool showLegend { get; set; }
    278             public bool showLimits { get; set; }
    279             public TimeUnit timeUnit { get; set; } = TimeUnit.Seconds;
    280             public GUIContent valueUnit { get; set; }
    281             ////REVIEW: should this be per timeline?
    282             public int timelineCount => m_Timelines != null ? m_Timelines.Length : 0;
    283             public int historyDepth { get; set; } = 100;
    284 
    285             public Vector2 limitsY
    286             {
    287                 get => m_LimitsY;
    288                 set
    289                 {
    290                     m_LimitsY = value;
    291                     m_LimitsYMin = null;
    292                     m_LimitsYMax = null;
    293                 }
    294             }
    295 
    296             public TimelineVisualizer(float totalTimeUnitsShown = 4)
    297             {
    298                 m_TotalTimeUnitsShown = totalTimeUnitsShown;
    299             }
    300 
    301             public override void OnDraw(Rect rect)
    302             {
    303                 var endTime = Time.realtimeSinceStartup;
    304                 var startTime = endTime - m_TotalTimeUnitsShown;
    305                 var endFrame = InputState.updateCount;
    306                 var startFrame = endFrame - (int)m_TotalTimeUnitsShown;
    307 
    308                 for (var i = 0; i < timelineCount; ++i)
    309                 {
    310                     var timeline = m_Timelines[i];
    311                     var sampleCount = timeUnit == TimeUnit.Frames
    312                         ? timeline.frameSamples.count
    313                         : timeline.timeSamples.count;
    314 
    315                     // Set up clip rect so that we can do stuff like render lines to samples
    316                     // falling outside the render rectangle and have them get clipped.
    317                     GUI.BeginGroup(rect);
    318                     var plotType = timeline.plotType;
    319                     var lastPos = default(Vector2);
    320                     var timeUnitsPerPixel = rect.width / m_TotalTimeUnitsShown;
    321                     var color = m_Timelines[i].color;
    322                     for (var n = sampleCount - 1; n >= 0; --n)
    323                     {
    324                         var sample = timeUnit == TimeUnit.Frames
    325                             ? timeline.frameSamples[n].value
    326                             : timeline.timeSamples[n].value;
    327 
    328                         ////TODO: respect limitsY
    329 
    330                         float y;
    331                         if (sample.isEmpty)
    332                             y = 0.5f;
    333                         else
    334                             y = sample.ToSingle();
    335 
    336                         y /= limitsY.y;
    337 
    338                         var deltaTime = timeUnit == TimeUnit.Frames
    339                             ? timeline.frameSamples[n].frame - startFrame
    340                             : timeline.timeSamples[n].time - startTime;
    341                         var pos = new Vector2(deltaTime * timeUnitsPerPixel, rect.height - y * rect.height);
    342 
    343                         if (plotType == PlotType.LineGraph)
    344                         {
    345                             if (n != sampleCount - 1)
    346                             {
    347                                 DrawLine(lastPos, pos, color, 2);
    348                                 if (pos.x < 0)
    349                                     break;
    350                             }
    351                         }
    352                         else if (plotType == PlotType.BarChart)
    353                         {
    354                             ////TODO: make rectangles have a progressively stronger hue or saturation
    355                             var barRect = new Rect(pos.x, pos.y, timeUnitsPerPixel, y * limitsY.y * rect.height);
    356                             DrawRectangle(barRect, color);
    357                         }
    358 
    359                         lastPos = pos;
    360                     }
    361                     GUI.EndGroup();
    362                 }
    363 
    364                 if (showLegend && timelineCount > 0)
    365                 {
    366                     var legendRect = rect;
    367                     legendRect.x += rect.width + 2;
    368                     legendRect.width = 400;
    369                     legendRect.height = ValueTextStyle.CalcHeight(m_Timelines[0].name, 400);
    370                     for (var i = 0; i < m_Timelines.Length; ++i)
    371                     {
    372                         var colorTagRect = legendRect;
    373                         colorTagRect.width = 5;
    374                         var labelRect = legendRect;
    375                         labelRect.x += 8;
    376                         labelRect.width -= 8;
    377 
    378                         DrawRectangle(colorTagRect, m_Timelines[i].color);
    379                         DrawText(m_Timelines[i].name, labelRect.position, ValueTextStyle);
    380 
    381                         legendRect.y += labelRect.height + 2;
    382                     }
    383                 }
    384 
    385                 if (showLimits)
    386                 {
    387                     if (m_LimitsYMax == null)
    388                         m_LimitsYMax = new GUIContent(m_LimitsY.y.ToString());
    389                     if (m_LimitsYMin == null)
    390                         m_LimitsYMin = new GUIContent(m_LimitsY.x.ToString());
    391 
    392                     DrawText(m_LimitsYMax, new Vector2(rect.x + rect.width, rect.y), ValueTextStyle);
    393                     DrawText(m_LimitsYMin, new Vector2(rect.x + rect.width, rect.y + rect.height), ValueTextStyle);
    394                 }
    395             }
    396 
    397             public override void AddSample(object value, double time)
    398             {
    399                 if (timelineCount == 0)
    400                     throw new InvalidOperationException("Must have set up a timeline first");
    401                 AddSample(0, PrimitiveValue.FromObject(value), (float)time);
    402             }
    403 
    404             public int AddTimeline(string name, Color color, PlotType plotType = default)
    405             {
    406                 var timeline = new Timeline
    407                 {
    408                     name = new GUIContent(name),
    409                     color = color,
    410                     plotType = plotType,
    411                 };
    412                 if (timeUnit == TimeUnit.Frames)
    413                     timeline.frameSamples = new RingBuffer<FrameSample>(historyDepth);
    414                 else
    415                     timeline.timeSamples = new RingBuffer<TimeSample>(historyDepth);
    416 
    417                 var index = timelineCount;
    418                 Array.Resize(ref m_Timelines, timelineCount + 1);
    419                 m_Timelines[index] = timeline;
    420 
    421                 return index;
    422             }
    423 
    424             public int GetTimeline(string name)
    425             {
    426                 for (var i = 0; i < timelineCount; ++i)
    427                     if (string.Compare(m_Timelines[i].name.text, name, StringComparison.InvariantCultureIgnoreCase) == 0)
    428                         return i;
    429                 return -1;
    430             }
    431 
    432             // Add a time-based sample.
    433             public void AddSample(int timelineIndex, PrimitiveValue value, float time)
    434             {
    435                 m_Timelines[timelineIndex].timeSamples.Append(new TimeSample
    436                 {
    437                     value = value,
    438                     time = time
    439                 });
    440             }
    441 
    442             // Add a frame-based sample.
    443             public ref PrimitiveValue GetOrCreateSample(int timelineIndex, int frame)
    444             {
    445                 ref var timeline = ref m_Timelines[timelineIndex];
    446                 ref var samples = ref timeline.frameSamples;
    447                 var count = samples.count;
    448                 if (count > 0)
    449                 {
    450                     if (samples[count - 1].frame == frame)
    451                         return ref samples[count - 1].value;
    452 
    453                     Debug.Assert(samples[count - 1].frame < frame, "Frame numbers must be ascending");
    454                 }
    455 
    456                 return ref samples.Append(new FrameSample {frame = frame}).value;
    457             }
    458 
    459             private float m_TotalTimeUnitsShown;
    460             private Vector2 m_LimitsY = new Vector2(-1, 1);
    461             private GUIContent m_LimitsYMin;
    462             private GUIContent m_LimitsYMax;
    463             private Timeline[] m_Timelines;
    464 
    465             private struct TimeSample
    466             {
    467                 public PrimitiveValue value;
    468                 public float time;
    469             }
    470 
    471             private struct FrameSample
    472             {
    473                 public PrimitiveValue value;
    474                 public int frame;
    475             }
    476 
    477             private struct Timeline
    478             {
    479                 public GUIContent name;
    480                 public Color color;
    481                 public RingBuffer<TimeSample> timeSamples;
    482                 public RingBuffer<FrameSample> frameSamples;
    483                 public PrimitiveValue minValue;
    484                 public PrimitiveValue maxValue;
    485                 public PlotType plotType;
    486             }
    487 
    488             public enum PlotType
    489             {
    490                 LineGraph,
    491                 BarChart,
    492             }
    493 
    494             public enum TimeUnit
    495             {
    496                 Seconds,
    497                 Frames,
    498             }
    499         }
    500 
    501         public static void DrawAxis(Axis axis, Rect rect, Color color = default, float width = 1)
    502         {
    503             Vector2 start, end, tickOffset;
    504             switch (axis)
    505             {
    506                 case Axis.X:
    507                     start = new Vector2(rect.x, rect.y + rect.height / 2);
    508                     end = new Vector2(start.x + rect.width, rect.y + rect.height / 2);
    509                     tickOffset = new Vector2(0, 3);
    510                     break;
    511 
    512                 case Axis.Y:
    513                     start = new Vector2(rect.x + rect.width / 2, rect.y);
    514                     end = new Vector2(start.x, rect.y + rect.height);
    515                     tickOffset = new Vector2(3, 0);
    516                     break;
    517 
    518                 case Axis.Z:
    519                     // From bottom left corner to upper right corner.
    520                     start = new Vector2(rect.x, rect.yMax);
    521                     end = new Vector2(rect.xMax, rect.y);
    522                     tickOffset = new Vector2(1.5f, 1.5f);
    523                     break;
    524 
    525                 default:
    526                     throw new NotImplementedException();
    527             }
    528 
    529             ////TODO: label limits
    530 
    531             DrawLine(start, end, color, width);
    532             DrawLine(start - tickOffset, start + tickOffset, color, width);
    533             DrawLine(end - tickOffset, end + tickOffset, color, width);
    534         }
    535 
    536         public static void DrawRectangle(Rect rect, Color color)
    537         {
    538             var savedColor = GUI.color;
    539             GUI.color = color;
    540             GUI.DrawTexture(rect, OnePixTex);
    541             GUI.color = savedColor;
    542         }
    543 
    544         public static void DrawText(string text, Vector2 pos, GUIStyle style)
    545         {
    546             var content = new GUIContent(text);
    547             DrawText(content, pos, style);
    548         }
    549 
    550         public static void DrawText(GUIContent text, Vector2 pos, GUIStyle style)
    551         {
    552             var content = new GUIContent(text);
    553             var size = style.CalcSize(content);
    554             var rect = new Rect(pos.x, pos.y, size.x, size.y);
    555             style.Draw(rect, content, false, false, false, false);
    556         }
    557 
    558         // Adapted from http://wiki.unity3d.com/index.php?title=DrawLine
    559         public static void DrawLine(Vector2 pointA, Vector2 pointB, Color color = default, float width = 1)
    560         {
    561             // Save the current GUI matrix, since we're going to make changes to it.
    562             var matrix = GUI.matrix;
    563 
    564             // Store current GUI color, so we can switch it back later,
    565             // and set the GUI color to the color parameter
    566             var savedColor = GUI.color;
    567             GUI.color = color;
    568 
    569             // Determine the angle of the line.
    570             var angle = Vector3.Angle(pointB - pointA, Vector2.right);
    571 
    572             // Vector3.Angle always returns a positive number.
    573             // If pointB is above pointA, then angle needs to be negative.
    574             if (pointA.y > pointB.y)
    575                 angle = -angle;
    576 
    577             // Use ScaleAroundPivot to adjust the size of the line.
    578             // We could do this when we draw the texture, but by scaling it here we can use
    579             //  non-integer values for the width and length (such as sub 1 pixel widths).
    580             // Note that the pivot point is at +.5 from pointA.y, this is so that the width of the line
    581             //  is centered on the origin at pointA.
    582             GUIUtility.ScaleAroundPivot(new Vector2((pointB - pointA).magnitude, width), new Vector2(pointA.x, pointA.y + 0.5f));
    583 
    584             // Set the rotation for the line.
    585             //  The angle was calculated with pointA as the origin.
    586             GUIUtility.RotateAroundPivot(angle, pointA);
    587 
    588             // Finally, draw the actual line.
    589             // We're really only drawing a 1x1 texture from pointA.
    590             // The matrix operations done with ScaleAroundPivot and RotateAroundPivot will make this
    591             //  render with the proper width, length, and angle.
    592             GUI.DrawTexture(new Rect(pointA.x, pointA.y, 1, 1), OnePixTex);
    593 
    594             // We're done.  Restore the GUI matrix and GUI color to whatever they were before.
    595             GUI.matrix = matrix;
    596             GUI.color = savedColor;
    597         }
    598 
    599         private static Texture2D s_OnePixTex;
    600         private static GUIStyle s_ValueTextStyle;
    601 
    602         internal static GUIStyle ValueTextStyle
    603         {
    604             get
    605             {
    606                 if (s_ValueTextStyle == null)
    607                 {
    608                     s_ValueTextStyle = new GUIStyle();
    609                     s_ValueTextStyle.fontSize -= 2;
    610                     s_ValueTextStyle.normal.textColor = Color.white;
    611                 }
    612                 return s_ValueTextStyle;
    613             }
    614         }
    615 
    616         internal static Texture2D OnePixTex
    617         {
    618             get
    619             {
    620                 if (s_OnePixTex == null)
    621                     s_OnePixTex = new Texture2D(1, 1);
    622                 return s_OnePixTex;
    623             }
    624         }
    625 
    626         public struct RingBuffer<TValue>
    627         {
    628             public TValue[] array;
    629             public int head;
    630             public int count;
    631 
    632             public RingBuffer(int size)
    633             {
    634                 array = new TValue[size];
    635                 head = 0;
    636                 count = 0;
    637             }
    638 
    639             public ref TValue Append(TValue value)
    640             {
    641                 int index;
    642                 var bufferSize = array.Length;
    643                 if (count < bufferSize)
    644                 {
    645                     Debug.Assert(head == 0, "Head can't have moved if buffer isn't full yet");
    646                     index = count;
    647                     ++count;
    648                 }
    649                 else
    650                 {
    651                     // Buffer is full. Bump head.
    652                     index = (head + count) % bufferSize;
    653                     ++head;
    654                 }
    655                 array[index] = value;
    656                 return ref array[index];
    657             }
    658 
    659             public ref TValue this[int index]
    660             {
    661                 get
    662                 {
    663                     if (index < 0 || index >= count)
    664                         throw new ArgumentOutOfRangeException(nameof(index));
    665                     return ref array[(head + index) % array.Length];
    666                 }
    667             }
    668         }
    669     }
    670 }